/*-------------------------------------------------------------------------
 *
 * passwordcheck.c
 *
 *
 * Copyright (c) 2009-2012, PostgreSQL Global Development Group
 *
 * Author: Laurenz Albe <laurenz.albe@wien.gv.at>
 *
 * IDENTIFICATION
 *	  contrib/passwordcheck/passwordcheck.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"
#include "knl/knl_variable.h"

#include <ctype.h>

#ifdef USE_CRACKLIB
#include <crack.h>
#endif

#include "commands/user.h"
#include "fmgr.h"
#include "libpq/md5.h"

PG_MODULE_MAGIC;

/* passwords shorter than this will be rejected */
#define MIN_PWD_LENGTH 8

extern "C" void _PG_init(void);

/*
 * check_password
 *
 * performs checks on an encrypted or unencrypted password
 * ereport's if not acceptable
 *
 * username: name of role being created or changed
 * password: new password (possibly already encrypted)
 * password_type: PASSWORD_TYPE_PLAINTEXT or PASSWORD_TYPE_MD5 (there
 *			could be other encryption schemes in future)
 * validuntil_time: password expiration time, as a timestamptz Datum
 * validuntil_null: true if password expiration time is NULL
 *
 * This sample implementation doesn't pay any attention to the password
 * expiration time, but you might wish to insist that it be non-null and
 * not too far in the future.
 */
static void check_password(
    const char* username, const char* password, int password_type, Datum validuntil_time, bool validuntil_null)
{
    int namelen = strlen(username);
    int pwdlen = strlen(password);
    char encrypted[MD5_PASSWD_LEN + 1];
    int i;
    bool pwd_has_letter = false;
    bool pwd_has_nonletter = false;

    switch (password_type) {
        case PASSWORD_TYPE_MD5:

            /*
             * Unfortunately we cannot perform exhaustive checks on encrypted
             * passwords - we are restricted to guessing. (Alternatively, we
             * could insist on the password being presented non-encrypted, but
             * that has its own security disadvantages.)
             *
             * We only check for username = password.
             */
            if (!pg_md5_encrypt(username, username, namelen, encrypted))
                elog(ERROR, "password encryption failed");
            if (strcmp(password, encrypted) == 0)
                ereport(
                    ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("password must not contain user name")));
            break;

        case PASSWORD_TYPE_PLAINTEXT:

            /*
             * For unencrypted passwords we can perform better checks
             */

            /* enforce minimum length */
            if (pwdlen < MIN_PWD_LENGTH)
                ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("password is too short")));

            /* check if the password contains the username */
            if (strstr(password, username))
                ereport(
                    ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("password must not contain user name")));

            /* check if the password contains both letters and non-letters */
            pwd_has_letter = false;
            pwd_has_nonletter = false;
            for (i = 0; i < pwdlen; i++) {
                /*
                 * isalpha() does not work for multibyte encodings but let's
                 * consider non-ASCII characters non-letters
                 */
                if (isalpha((unsigned char)password[i]))
                    pwd_has_letter = true;
                else
                    pwd_has_nonletter = true;
            }
            if (!pwd_has_letter || !pwd_has_nonletter)
                ereport(ERROR,
                    (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
                        errmsg("password must contain both letters and nonletters")));

#ifdef USE_CRACKLIB
            /* call cracklib to check password */
            if (FascistCheck(password, CRACKLIB_DICTPATH))
                ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("password is easily cracked")));
#endif
            break;

        default:
            elog(ERROR, "unrecognized password type: %d", password_type);
            break;
    }

    /* all checks passed, password is ok */
}

/*
 * Module initialization function
 */
void _PG_init(void)
{
    /* activate password checks when the module is loaded */
    check_password_hook = check_password;
}
